﻿//-----------------------------------------------------------------------
// <copyright file="ActionManager.cs" company="Nathan Miller">
// Copyright (c) Nathan Miller.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//-----------------------------------------------------------------------
namespace TileTool.Base.Action
{
    using System;
    using System.Collections.Generic;
    using TileTool.Action;
    using TileTool.Action.Message;
    using TileTool.Log;
    using TileTool.Message;

    /// <summary>
    /// Action manager implementation.
    /// </summary>
    public class ActionManager : IActionManager
    {
        #region Constants - Private

        /// <summary>
        /// Log group for this class.
        /// </summary>
        private const string LogGroup = "ActionManager";

        #endregion

        #region Fields - Private

        /// <summary>
        /// Actions that we can undo.
        /// </summary>
        private Stack<IAction> undoActionChain = new Stack<IAction>();

        /// <summary>
        /// Actions that we can redo.
        /// </summary>
        private Stack<IAction> redoActionChain = new Stack<IAction>();

        /// <summary>
        /// Message router to send messages on.
        /// </summary>
        private MessageRouter messageRouter;

        #endregion

        #region Constructor

        /// <summary>
        /// Constructor for the action manager.
        /// </summary>
        /// <param name="router">Message router to use in the action manager.</param>
        public ActionManager(MessageRouter router)
        {
            if (router == null)
            {
                throw new ArgumentNullException("router", "Message router must be non-null.");
            }

            this.messageRouter = router;
        }

        #endregion

        #region Properties - Public

        /// <summary>
        /// Gets if the action manager can undo an action.
        /// </summary>
        public bool CanUndo
        {
            get
            {
                return this.undoActionChain.Count != 0;
            }
        }

        /// <summary>
        /// Gets if the action manager can redo an action.
        /// </summary>
        public bool CanRedo
        {
            get
            {
                return this.redoActionChain.Count != 0;
            }
        }
        
        #endregion

        #region Methods - Public

        /// <summary>
        /// Clear all actions that the action manager has.
        /// </summary>
        public void Clear()
        {
            this.undoActionChain.Clear();
            this.redoActionChain.Clear();
        }

        /// <summary>
        /// Add the passed action to the manager. Action will not have Do() called on it,
        /// since it is assumed that Do() has been called prior to add.
        /// 
        /// A IActionUndoRedoMessage message will be sent that an action was added and
        /// that it had Do() called on it.
        /// </summary>
        /// <param name="action">Action to add.</param>
        public void Add(IAction action)
        {
            if (action == null)
            {
                throw new ArgumentNullException("action", "Action must be non-null.");
            }

            LogSystem.LogInfo(LogGroup, "Adding action " + action.GetType().Name + ". Action is valid: " + action.Valid);

            this.redoActionChain.Clear();
            this.undoActionChain.Push(action);

            this.SendActionUndoRedoMessage(action, true, true);
        }

        /// <summary>
        /// Do the action that is next in the action chain.
        /// </summary>
        public void Redo()
        {
            LogSystem.LogInfo(LogGroup, "Redo()");

            if (this.redoActionChain.Count == 0)
            {
                LogSystem.LogInfo(LogGroup, "Redo() - Nothing to redo.");
                return;
            }

            IAction action;

            do
            {
                action = this.redoActionChain.Pop();
                this.undoActionChain.Push(action);

                if (!action.Valid)
                {
                    LogSystem.LogInfo(LogGroup, "Redo() - Ignoring action " + action.GetType().Name);
                }
            }
            while (this.redoActionChain.Count != 0 && !action.Valid);

            if (action.Valid)
            {
                LogSystem.LogInfo(LogGroup, "Redo() - Redo action " + action.GetType().Name);

                action.Do();

                this.SendActionUndoRedoMessage(action, true);
            }
        }

        /// <summary>
        /// Undo the last action that was done.
        /// </summary>
        public void Undo()
        {
            LogSystem.LogInfo(LogGroup, "Undo()");

            if (this.undoActionChain.Count == 0)
            {
                LogSystem.LogInfo(LogGroup, "Undo() - Nothing to redo.");

                return;
            }

            IAction action;
            
            do 
            {
                action = this.undoActionChain.Pop();
                this.redoActionChain.Push(action);

                if (!action.Valid)
                {
                    LogSystem.LogInfo(LogGroup, "Redo() - Ignoring action " + action.GetType().Name);
                }
            }
            while (this.undoActionChain.Count != 0 && !action.Valid);

            if (action.Valid)
            {
                LogSystem.LogInfo(LogGroup, "Redo action " + action.GetType().Name);

                action.Undo();

                this.SendActionUndoRedoMessage(action, false);
            }
        }

        #endregion

        #region Methods - Private

        /// <summary>
        /// Send an action undo / redo message.
        /// </summary>
        /// <param name="action">Action we were using.</param>
        /// <param name="actionDoCalled">true if do was called on the action. false if Undo() was called.</param>
        /// <param name="initialDo">true if this is the initial Do() called on the action when it is added.</param>
        private void SendActionUndoRedoMessage(IAction action, bool actionDoCalled, bool initialDo = false)
        {
            ActionUndoRedoMessage msg = new ActionUndoRedoMessage(action);

            msg.InitialDo = initialDo;
            msg.DoCalled = actionDoCalled;
            msg.CanRedo = this.CanRedo;
            msg.CanUndo = this.CanUndo;

            this.messageRouter.SendMessage(msg);
        }

        #endregion
    }
}
